﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Kindler.IO.MobiFile
{
    public class MobiFileReader : IMobiFileReader
    {
        private static readonly DateTime baseDate = new DateTime(1970, 1, 1);

        public MobiFile Read(byte[] fileBytes)
        {
            var file = Create<MobiFile>(fileBytes);
            file.AllBytes = fileBytes;
            return file;
        }

        internal static T Create<T>(byte[] bytes) where T : new()
        {
            var target = new T();

            var mappings = from prop in typeof(T).GetProperties()
                           let propIndicator = prop.GetCustomAttributes(false).OfType<MobiPropertyAttribute>().FirstOrDefault()
                           where prop.CanWrite && propIndicator != null
                           orderby propIndicator.PropertyIndex
                           select new
                           {
                               Property = prop,
                               Index = propIndicator.PropertyIndex,
                               LengthInBytes = propIndicator.LengthInBytes,
                               LengthProperty = propIndicator.LengthProperty
                           };

            int bytesRead = 0;

            foreach (var propMapping in mappings)
            {
                int byteLength = GetByteLength(target, bytes, propMapping.LengthInBytes, propMapping.LengthProperty, propMapping.Property);
                byte[] valueBytes = bytes.Skip(bytesRead).Take(byteLength).ToArray();
                propMapping.Property.SetValue(target, GetValue(target, valueBytes, propMapping.Property.PropertyType), null);
                bytesRead += valueBytes.Length;
            }

            Trace.TraceInformation("Created instance of '{0}'. Total Bytes: {1}", typeof(T), bytesRead);

            return target;
        }

        private static object GetValue(object target, byte[] bytes, Type convertTo)
        {
            if (convertTo == typeof(DateTime))
            {
                return baseDate.AddSeconds(UInt32FromBigEndian(bytes));
            }
            else if (convertTo == typeof(string))
            {
                return Encoding.UTF8.GetString(bytes).TrimEnd('\0');
            }
            else if (convertTo == typeof(UInt16))
            {
                return UInt16FromBigEndian(bytes);
            }
            else if (convertTo == typeof(UInt32))
            {
                return UInt32FromBigEndian(bytes);
            }
            else if (convertTo.IsSubclassOf(typeof(Enum)))
            {
                if (bytes.Length == 4)
                {
                    return Enum.ToObject(convertTo, UInt32FromBigEndian(bytes));
                }
                else
                {
                    return Enum.ToObject(convertTo, UInt16FromBigEndian(bytes));
                }
            }
            else if (convertTo == typeof(byte))
            {
                return bytes[0];
            }
            else if (convertTo == typeof(byte[]))
            {
                return bytes;
            }
            else if (convertTo == typeof(IEnumerable<MobiFileRecordMetadata>))
            {
                var file = target as MobiFile;

                if (file != null)
                {
                    var list = new List<MobiFileRecordMetadata>();

                    for (int i = 0; i < file.NumberOfRecords; i++)
                    {
                        list.Add(Create<MobiFileRecordMetadata>(bytes.Skip(i * 8).Take(8).ToArray()));
                    }

                    return list;
                }
            }
            else if (convertTo == typeof(IEnumerable<MobiFileRecord>))
            {
                var file = target as MobiFile;

                if (file != null)
                {
                    var list = new List<MobiFileRecord>();
                    int offsetFix = 78 + (file.NumberOfRecords * 8);

                    for (int i = 0; i < file.NumberOfRecords; i++)
                    {
                        MobiFileRecordMetadata metadata = file.RecordMetadata.ElementAtOrDefault(i);
                        MobiFileRecordMetadata nextMetadata = file.RecordMetadata.ElementAtOrDefault(i + 1);
                        int end = (nextMetadata != null) ? (int)nextMetadata.Offset : bytes.Length;

                        list.Add(new MobiFileRecord { Value = bytes.Skip((int)(metadata.Offset - offsetFix)).Take((int)(end - metadata.Offset)).ToArray() });
                    }

                    return list;
                }
            }
            else if (convertTo == typeof(IEnumerable<ExthRecord>))
            {
                var header = target as MobiFileHeader;

                if (header != null)
                {
                    var list = new List<ExthRecord>();
                    int offset = 0;

                    for (int i = 0; i < header.ExthRecordCount; i++)
                    {
                        int length = (int)UInt32FromBigEndian(bytes.Skip(offset + 4).Take(4).ToArray());
                        var record = Create<ExthRecord>(bytes.Skip(offset).Take(length).ToArray());
                        list.Add(record);

                        offset += length;
                    }

                    return list;
                }
            }

            throw new NotSupportedException(string.Format("The type '{0}' is not supported for target type '{0}'.", convertTo, target.GetType()));
        }

        private static UInt32 UInt32FromBigEndian(byte[] bytes)
        {
            long ret = 0;

            for (int i = 0; i < bytes.Length; i++)
            {
                ret = unchecked((ret << 8) | bytes[i]);
            }

            return (UInt32)ret;
        }

        private static UInt16 UInt16FromBigEndian(byte[] bytes)
        {
            long ret = 0;

            for (int i = 0; i < 2; i++)
            {
                ret = unchecked((ret << 8) | bytes[i]);
            }

            return (UInt16)ret;
        }

        private static int GetByteLength(object target, byte[] allbytes, int lengthInBytes, string lengthProperty, PropertyInfo property)
        {
            if (lengthInBytes > 0)
            {
                return lengthInBytes;
            }

            if (!string.IsNullOrEmpty(lengthProperty))
            {
                PropertyInfo lengthPropertyInfo = target.GetType().GetProperty(lengthProperty);

                if (lengthPropertyInfo == null)
                {
                    throw new ArgumentException(string.Format("The property '{0}' does not exist in the type '{1}'",
                        lengthProperty, target.GetType()));
                }

                return (int)lengthPropertyInfo.GetValue(target, null);
            }

            if (property.PropertyType == typeof(string))
            {
                throw new ArgumentException("Properties with type 'string' must have LengthInBytes specified.");
            }
            else if (property.PropertyType == typeof(UInt16))
            {
                return 2;
            }
            else if (property.PropertyType == typeof(UInt32))
            {
                return 4;
            }
            else if (property.PropertyType.IsSubclassOf(typeof(Enum)))
            {
                return 2;
            }
            else if (property.PropertyType == typeof(DateTime))
            {
                return 4;
            }
            else if (property.PropertyType == typeof(byte))
            {
                return 1;
            }
            else if (property.PropertyType == typeof(IEnumerable<MobiFileRecordMetadata>))
            {
                var file = target as MobiFile;

                if (file != null)
                {
                    return file.NumberOfRecords * 8; // metadata takes up 8 bytes and there is metadata for each record
                }
            }
            else if (property.PropertyType == typeof(IEnumerable<MobiFileRecord>))
            {
                var file = target as MobiFile;

                if (file != null)
                {
                    // records run to the end of the file
                    return (int)(allbytes.Length - file.RecordMetadata.First().Offset);
                }
            }
            else if (property.PropertyType == typeof(IEnumerable<ExthRecord>))
            {
                var header = target as MobiFileHeader;

                if (header != null)
                {
                    // records run to the end of the file
                    return (int)header.ExthHeaderLength - 8;
                }
            }

            throw new NotSupportedException(string.Format("Properties with type '{0}' are not supported.", property.PropertyType));
        }
    }
}